package com.appengine.dockstats;

/**
 * XML utils, including formatting.
 */
public class XmlUtils {
    private static XmlFormatter formatter = new XmlFormatter(2, 80);

    public static String formatXml(String s) {
        return formatter.format(s, 0);
    }

    public static String formatXml(String s, int initialIndent) {
        return formatter.format(s, initialIndent);
    }

    private static String buildWhitespace(int numChars) {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < numChars; i++) {
            sb.append(" ");
        }

        return sb.toString();
    }

    /**
     * Wraps the supplied text to the specified line length.
     * @lineLength the maximum length of each line in the returned string (not including indent if specified).
     * @indent optional number of whitespace characters to prepend to each line before the text.
     * @linePrefix optional string to append to the indent (before the text).
     * @returns the supplied text wrapped so that no line exceeds the specified line length + indent, optionally with
     * indent and prefix applied to each line.
     */
    private static String lineWrap(String s, int lineLength, Integer indent, String linePrefix) {
        if (s == null) {
            return null;
        }

        StringBuilder sb           = new StringBuilder();
        int           lineStartPos = 0;
        int           lineEndPos;
        boolean       firstLine = true;

        while (lineStartPos < s.length()) {
            if (!firstLine) {
                sb.append("\n");
            } else {
                firstLine = false;
            }

            if (lineStartPos + lineLength > s.length()) {
                lineEndPos = s.length() - 1;
            } else {
                lineEndPos = lineStartPos + lineLength - 1;

                while ((lineEndPos > lineStartPos)
                        && ((s.charAt(lineEndPos) != ' ') && (s.charAt(lineEndPos) != '\t'))) {
                    lineEndPos--;
                }
            }

            sb.append(buildWhitespace(indent));

            if (linePrefix != null) {
                sb.append(linePrefix);
            }

            sb.append(s.substring(lineStartPos, lineEndPos + 1));
            lineStartPos = lineEndPos + 1;
        }

        return sb.toString();
    }

    private static class XmlFormatter {
        private int     indentNumChars;
        private int     lineLength;
        private boolean singleLine;

        public XmlFormatter(int indentNumChars, int lineLength) {
            this.indentNumChars = indentNumChars;
            this.lineLength     = lineLength;
        }

        public synchronized String format(String s, int initialIndent) {
            int           indent = initialIndent;
            StringBuilder sb     = new StringBuilder();

            for (int i = 0; i < s.length(); i++) {
                char currentChar = s.charAt(i);

                if (currentChar == '<') {
                    char nextChar = s.charAt(i + 1);

                    if (nextChar == '/') {
                        indent -= indentNumChars;
                    }

                    if (!singleLine) {    // Don't indent before closing element if we're creating opening and closing elements on a single line.
                        sb.append(buildWhitespace(indent));
                    }

                    if ((nextChar != '?') && (nextChar != '!') && (nextChar != '/')) {
                        indent += indentNumChars;
                    }

                    singleLine = false;    // Reset flag.
                }

                sb.append(currentChar);

                if (currentChar == '>') {
                    if (s.charAt(i - 1) == '/') {
                        indent -= indentNumChars;
                        sb.append("\n");
                    } else {
                        int nextStartElementPos = s.indexOf('<', i);

                        if (nextStartElementPos > i + 1) {
                            String textBetweenElements = s.substring(i + 1, nextStartElementPos);

                            // If the space between elements is solely newlines, let them through to preserve additional newlines in source document.
                            if (textBetweenElements.replaceAll("\n", "").length() == 0) {
                                sb.append(textBetweenElements + "\n");
                            }

                            // Put tags and text on a single line if the text is short.
                            else if (textBetweenElements.length() <= lineLength * 0.5) {
                                sb.append(textBetweenElements);
                                singleLine = true;
                            }

                            // For larger amounts of text, wrap lines to a maximum line length.
                            else {
                                sb.append("\n" + lineWrap(textBetweenElements, lineLength, indent, null) + "\n");
                            }

                            i = nextStartElementPos - 1;
                        } else {
                            sb.append("\n");
                        }
                    }
                }
            }

            return sb.toString();
        }
    }
}
